package nl.utwente.ewi.hmi.multitouch.io;

import java.awt.Dimension;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;

import nl.utwente.ewi.hmi.multitouch.Tangible;
import nl.utwente.ewi.hmi.multitouch.TouchDevice;
import nl.utwente.ewi.hmi.multitouch.TouchDeviceInfo;
import nl.utwente.ewi.hmi.multitouch.analysis.Segment;
import nl.utwente.ewi.hmi.multitouch.analysis.segmentation.FrameSegmentation;

/**
 * The TouchStream provides the minimal interface for a {@linkplain TouchDevice}. By providing the ability
 * to read {@linkplain Frame Frames} the {@linkplain TouchDevice} can find and track touches, and discover
 * extra data (pressure, distance) about touches as well.
 * 
 * @author Michiel Hakvoort
 * @version 1.0
 */
public abstract class TouchStream {

	/**
	 * The type of touch that has occurred.
	 * POSITIVE indicates there is a touch.
	 * NEGATIVE indicates there is not a touch.
	 * NEVER indicates there is no touch, there never was a touch and there never will be a touch.
	 * UNCERTAIN indicates there is a touch, but the touch itself is ambiguous.
	 * 
	 * @author Michiel Hakvoort
	 * @version 1.0
	 */
	public enum TouchType {
		POSITIVE,
		NEGATIVE,
		NEVER,
		UNCERTAIN
	}

	/**
	 * The FrameBuffer class is used to read {@linkplain Frame Frames} from a TouchStream. When reading
	 * frames from a TouchStream the read {@linkplain Frame Frames} are copied into the FrameBuffer. The
	 * FrameBuffer is dynamically sized, to prevent unnecessary allocations.
	 * , but stores as
	 * @author Michiel Hakvoort
	 * @version 1.0
	 */
	public static class FrameBuffer {

		/**
		 * The FrameBuffer's allocated Frames
		 */
		protected List<Frame> frames = null;

		/**
		 * The amount of live Frames currently buffered
		 */
		private int frameCount = 0;

		/**
		 * Create a new FrameBuffer
		 */
		public FrameBuffer() {
			frames = new ArrayList<Frame>();
		}

		/**
		 * Get the Frame at the specified index.
		 * 
		 * @param index
		 * @return The {@linkplain Frame} at the specified index, or null if no frame is at that index
		 */
		public Frame getFrame(int index) {
			if(index > frameCount) {
				return null;
			}

			return this.frames.get(index);
		}

		/**
		 * Get the amount of {@linkplain Frame Frames} in the FrameBuffer
		 * 
		 * @return The amount of {@linkplain Frame Frames} in the FrameBuffer
		 */
		public int getFrameCount() {
			return this.frameCount;
		}

		/**
		 * Write an array of {@linkplain Frame Frames} into the FrameBuffer.
		 * 
		 * @param frames The array of {@linkplain Frame Frames} to write
		 * @return The amount of written frames
		 */
		protected int write(Frame[] frames) {
			int frameCount = 0;

			for(int i = 0; i < frames.length; i++) {
				if(frames[i] == null) {
					continue;
				}
				frameCount++;
				Frame from = frames[i];
				Frame to = null;
				if(i < this.frames.size()) {
					to = this.frames.get(i);
				}
				
				if(to == null) {
					to = new Frame(from.getSize());
					if(i >= this.frames.size()) {
						this.frames.add(to);
					} else {
						this.frames.set(i, to);
					}
				}

				from.copy(to);
			}
			
			this.frameCount = frameCount;
			
			return frameCount;
		}

	}

	/**
	 * A Frame is a single snapshot of the input on a touch table. The normal input of a touch table is abstracted away
	 * into information about pressure, distance, type of touch and the tangible which interacts with the surface.  
	 * 
	 * @author Michiel Hakvoort
	 */
	public static class Frame {

		protected TouchType[] touchTypeMap = null;
		protected Tangible[] tangibleMap = null;

		protected Dimension size = null;

		/**
		 * Create a new Frame with the given size.
		 * The allocation of a new Frame will take 24 * size.width * size.height bytes 
		 * 
		 * @param size
		 */
		public Frame(Dimension size) {
			this.size = size;
			int dataLength = size.height * size.width;

			this.touchTypeMap = new TouchType[dataLength];
			
			this.tangibleMap = new Tangible[dataLength];
		}


		/**
		 * Update the Frame with new TouchTypes
		 * 
		 * @param touchTypeMap
		 */
		public void writeTouchTypeMap(TouchType[] touchTypeMap) {
			if(touchTypeMap.length != this.touchTypeMap.length) {
				throw new IllegalArgumentException("Array size does not match with Frame size");
			}
			System.arraycopy(touchTypeMap, 0, this.touchTypeMap, 0, this.touchTypeMap.length);
		}

		/**
		 * Update the Frame with new TouchTypes
		 * 
		 * @param tangibleMap
		 */
		public void writeTangibleMap(Tangible[] tangibleMap) {
			if(tangibleMap.length != this.tangibleMap.length) {
				throw new IllegalArgumentException("Array size does not match with Frame size");
			}
			System.arraycopy(tangibleMap, 0, this.tangibleMap, 0, this.tangibleMap.length);
		}
		
		/**
		 * Copy this Frame's data into the targeted Frame
		 * 
		 * @param target The target Frame
		 */
		protected void copy(Frame target) {
			target.writeTouchTypeMap(this.touchTypeMap);
			target.writeTangibleMap(this.tangibleMap);
		}

		/**
		 * Return the Frame's size
		 * @return
		 */
		public Dimension getSize() {
			return this.size;
		}

		/**
		 * Get the touch types of the Frame. The amount of touch types is equal
		 * to the Frame's height times the Frame's width. For any point (x,y) at the frame,
		 * the point is located at `y * the Frame's width + x`.
		 * 
		 * @return The touch types of the frame
		 */
		public TouchType[] getTouchTypeMap() {
			return this.touchTypeMap;
		}

		/**
		 * Get the tangibles of the frame. The {@linkplain Tangible} is what interacts
		 * with the surface. The amount of tangibles is equal to the Frame's height 
		 * times the Frame's width. For any point (x,y) at the frame, the point is
		 * located at `y * the Frame's width + x`.
		 * 
		 * @return The tangibles of the frame
		 */
		public Tangible[] getTangibleMap() {
			return this.tangibleMap;
		}
	}


	protected TouchDeviceInfo touchDeviceInfo = null;
	
	public TouchStream(TouchDeviceInfo touchDeviceInfo) {
		this.touchDeviceInfo = touchDeviceInfo;
	}

	/**
	 * Open the TouchStream.
	 * 
	 * @return True if the TouchStream is opened
	 * @throws IOException
	 */
	public abstract boolean open() throws IOException;
	
	/**
	 * Check whether the TouchStream is opened.
	 * 
	 * @return True when the TouchStream is opened
	 */
	public abstract boolean isOpen();
	
	/**
	 * Close the TouchStream.
	 * 
	 * @return True if the TouchStream is closed
	 * @throws IOException
	 */
	public abstract boolean close() throws IOException;

	// ís the touch device responsible for choosing the frame segmentation?
	public abstract void read(Set<Segment> segments) throws IOException;

	public abstract Set<Object> getActors();

	/**
	 * Return the {@linkplain TouchDeviceInfo} associated with the TouchStream.
	 * 
	 * @return The {@linkplain TouchDeviceInfo} associated with the TouchStream
	 */
	public TouchDeviceInfo getTouchDeviceInfo() {
		return this.touchDeviceInfo;
	}
}
